#!/usr/bin/perl -w
#
# $Id: Connection.pm,v 1.33 2004/11/26 08:41:42 solovam Exp $
#
# This file is a part of gkismet
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#

#
# Connection class
#
package Connection;

use IO::Socket;
use IO::Select;
use Gtk;
use Misc;
use Observable;
use strict;

my $timeout = 5;

#
# Inherit methods from Observable
#
@Connection::ISA = qw(Observable);

#
# Constructor
#
sub new
{
	# Standard prologue
	my $type = shift;
	my $self = {};
	bless $self, $type;

	# Constructor arguments
	$self->{'gKismetApplication'} = shift;
	$self->{'host'} = shift;
	$self->{'port'} = shift;

	# Connect to server
	if(!$self->connect())
	{
		return(undef);
	}

	# Enable protocols
	$self->enableProtocols();

	# Init fields
	$self->{'network'} = {};
	$self->{'status'} = {};
	$self->{'info'} = {};
	$self->{'packet'} = {};
	$self->{'string'} = {};
	$self->{'gps'} = {};
	$self->{'time'} = {};
	$self->{'alert'} = {};

	return($self);
}

#
# Destructor
#
sub DESTROY
{
	my $self = shift;
	$self->disconnect();
}

#
# Get socket
#
sub getSocket
{
	my $self = shift;
	return $self->{'socket'};
}

#
# Connect to server
#
sub connect
{
	my $self = shift;

	$self->{'socket'} = IO::Socket::INET->new("PeerAddr" => $self->{'host'}, "PeerPort" => $self->{'port'}, "Proto" => "tcp", "Timeout" => $timeout);
	unless($self->{'socket'})
	{
		return(0);
	}

	# Register input handler
	my $inputId = Gtk::Gdk->input_add($self->getSocket()->fileno, 'read', sub {$self->readServer()}, $self->getSocket());
	$self->{'inputId'} = $inputId;

	# Is it a kismet server we connected to?
	unless($self->{'socket'}->getline() =~ /\*KISMET/)
	{
		$self->disconnect();
		return(0);
	}

	return(1);
}

#
# Disconnect from server
#
sub disconnect
{
	my $self = shift;
	if(defined $self->{'inputId'})
	{
		Gtk::Gdk->input_remove($self->{'inputId'});
	}
	if($self->{'socket'})
	{
		$self->{'socket'}->close();
	}
}

#
# Enable default set of protocols to watch
#
sub enableProtocols
{
	my $self = shift;
	my @protocols = qw(GPS INFO NETWORK CLIENT REMOVE ALERT STATUS);
	for my $p (@protocols)
	{
		$self->{'socket'}->print("!0 ENABLE $p *\n");
	}
}

#
# Eanble strings
#
sub enableStrings
{
	my $self = shift;
	$self->{'socket'}->print("!0 ENABLE STRING *\n");
}

#
# Disable strings
#
sub disableStrings
{
	my $self = shift;
	$self->{'socket'}->print("!0 REMOVE STRING\n");
}

#
# Enable packets
#
sub enablePackets
{
	my $self = shift;
	$self->{'socket'}->print("!0 ENABLE PACKET *\n");
}

#
# Disable packets
#
sub disablePackets
{
	my $self = shift;
	$self->{'socket'}->print("!0 REMOVE PACKET\n");
}

#
# Ask server to print capabilities
#
sub askCaps
{
	my $self = shift;
	my @protocols = qw(GPS INFO NETWORK CLIENT REMOVE ALERT STATUS STRING PACKET);
	for my $p (@protocols)
	{
		$self->{'socket'}->print("!0 CAPABILITY $p\n");
	}
}

#
# Read and parse input
#
my $inReadServer = $false;
sub readServer 
{
	my $self = shift;

	if($inReadServer)
	{
		return;
	}
	$inReadServer = $true;

	my $res = $self->{'socket'}->getline();
	if(!$res)
	{
		$self->notifyObservers({'changed' => 'disconnected'});
	}
	else
	{
		chomp($res);
		my($cmd, $data) = ($res =~ /\*([^:]+):\s+(.*)/);

		if($cmd eq 'GPS')
		{
			my @fields = qw(lat lon alt spd heading fix);
			my @values = Connection->oneSplit($data);
			$self->{'gps'} = Connection->makeHash(\@fields, \@values);
			$self->notifyObservers({'changed' => 'gps'});
		}
		elsif($cmd eq 'INFO')
		{
			my @fields = qw(networks packets crypt weak noise dropped rate cardquality  cardpower cardnoise);
			my @values = Connection->oneSplit($data);
			$self->{'info'} = Connection->makeHash(\@fields, \@values);
			$self->notifyObservers({'changed' => 'info'});
		}
		elsif($cmd eq 'NETWORK')
		{
			my @fields = qw(bssid type ssid beaconinfo llcpackets datapackets cryptpackets weakpackets channel wep firsttime lasttime atype
				rangeip gpsfixed minlat minlon minalt minspd maxlat maxlon maxalt maxspd octets cloaked beaconrate maxrate
				manufkey manufscore quality signal noise bestquality bestsignal bestnoise bestlat bestlon bestalt agglat agglon
				aggalt aggpoints datasize turbocellnid turbocellmode turbocellsat carrierset maxseenrate encodingset decrypted dupeivpackets);
			my @values = Connection->oneSplit($data);
			my $bssid = $values[0];
			for(my $i = 0; $i <= $#fields; $i++)
			{
				$self->{'network'}{$bssid}{$fields[$i]} = defined $values[$i] ? $values[$i] : 0;
			}
			$self->notifyObservers({'changed' => 'network', 'bssid' => $bssid});
		}
		elsif($cmd eq 'CLIENT')
		{
			my @fields = qw(bssid mac type firsttime lasttime manufkey manufscore datapackets cryptpackets weakpackets gpsfixed minlat minlon
				minalt minspd maxlat maxlon maxalt maxspd agglat agglon aggalt aggpoints maxrate quality signal noise bestquality
				bestsignal bestnoise bestlat bestlon bestalt atype ip datasize maxseenrate encodingset decrypted);
			my @values = Connection->oneSplit($data);
			my($bssid, $mac) = @values[0,1];
			for(my $i = 0; $i <= $#fields; $i++)
			{
				$self->{'network'}{$bssid}{'client'}{$mac}{$fields[$i]} = defined $values[$i] ? $values[$i] : 0;
			}
			$self->notifyObservers({'changed' => 'client', 'bssid' => $bssid, 'mac' => $mac});
		}
		elsif($cmd eq 'REMOVE')
		{
			my $bssid = $data;
			if(defined $self->{'network'}{$bssid})
			{
				$self->notifyObservers({'changed' => 'remove', 'bssid' => $bssid});
				delete $self->{'network'}{$bssid};
			}
		}
		elsif($cmd eq 'ALERT')
		{
			my @fields = qw(sec usec header bssid source dest other channel text);
			my @values = Connection->oneSplit($data);
			$self->{'alert'} = Connection->makeHash(\@fields, \@values);
			$self->notifyObservers({'changed' => 'alert'});
		}
		elsif($cmd eq 'STATUS')
		{
			$self->{'status'} = $data;
			$self->notifyObservers({'changed' => 'status'});
		}
		elsif($cmd eq 'TIME')
		{
			$self->{'time'} = $data;
			$self->notifyObservers({'changed' => 'time'});
		}
		elsif($cmd eq 'PACKET')
		{
			# Should we kep more than one packet?
			my @fields = qw(type subtype timesec encrypted weak beaconrate sourcemac destmac bssid ssid prototype sourceip destip sourceport
				destport nbtype nbsource);
			my @values = Connection->oneSplit($data);
			$self->{'packet'} = Connection->makeHash(\@fields, \@values);
			$self->notifyObservers({'changed' => 'packet'});
		}
		elsif($cmd eq 'STRING')
		{
			# Should we kep more than one string?
			my @fields = qw(bssid sourcemac text);
			my @values = Connection->oneSplit($data);
			$self->{'string'} = Connection->makeHash(\@fields, \@values);
			$self->notifyObservers({'changed' => 'string'});
		}
	}
	$inReadServer = $false;
}

#
# Make hash from an array of keys and values 
#
sub makeHash
{
	shift;
	my $keys = shift;
	my $values = shift;
	my %hash;
	for(my $i = 0; $i <= $#{$keys}; $i++)
	{
			$hash{$keys->[$i]} = defined $values->[$i] ? $values->[$i] : 0;
	}
	return \%hash;
}

#
# Get port
#
sub port
{
	my $self = shift;
	return $self->{'port'};
}

#
# Get host
#
sub host
{
	my $self = shift;
	return $self->{'host'};
}

#
# Get network
#
sub getNetwork
{
	my $self = shift;
	return $self->{'network'};
}

#
# get status
#
sub getStatus
{
	my $self = shift;
	return $self->{'status'};
}

#
# Get info
#
sub getInfo
{
	my $self = shift;
	return $self->{'info'};
}

#
# Get GPS
#
sub getGps
{
	my $self = shift;
	return $self->{'gps'};
}

#
# Get alert
#
sub getAlert
{
	my $self = shift;
	return $self->{'alert'};
}

#
# Get time
#
sub getTime
{
	my $self = shift;
	return $self->{'time'};
}

#
# Get string
#
sub getString
{
	my $self = shift;
	return $self->{'string'};
}

#
# Get packet
#
sub getPacket
{
	my $self = shift;
	return $self->{'packet'};
}

#
# Split on space. \001 works as quotation symbol. Based on shellwords.pl
#
sub oneSplit
{
	shift;

	($_) = (@_);
	my(@words, $snippet, $field);

	s/^\s+//;
	while ($_ ne '')
	{
		$field = '';
		while(1)
		{
			if(s/^\001([^\001]*)\001//)
			{
				$snippet = $1;
			}
			elsif(s/^([^\s\001]+)//)
			{
				$snippet = $1;
			}
			else
			{
				s/^\s+//;
				last;
			}
			$field .= $snippet;
		}
		push(@words, $field);
	}
	return(@words);
}

1;
